﻿/* 
 * Copyright (c) 2011-12 Andrey Bulygin
 * 
 * This file is part of Interceptor.
 * Interceptor is free software: you can redistribute it and/or modify it under the terms of the GNU 
 * Library General Public License as published by the Free Software Foundation, either version 3 of the 
 * License, or (at your option) any later version.
 * 
 * Interceptor is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without 
 * even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See 
 * the GNU Library General Public License for more details. You should have received a copy of the GNU 
 * Library General Public License along with Interceptor. If not, write to the Free Software Foundation, Inc., 59 
 * Temple Place, Suite 330, Boston, MA 02111-1307 USA.
 */

using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.Reflection.Emit;
using System.Text;
using System.Text.RegularExpressions;
using Interceptor.Core.Exceptions;
using Interceptor.Core.Objects;

namespace Interceptor.Core
{
	internal class BaseFactory
	{
		private const int TARGET_PARAM_INDEX = 0;
		private const int CONTEXT_INVOKER_PARAM_INDEX = 1;
		private const int MAX_PARAMETER_MARK = 4;

		protected static MethodsMapping FindInterceptionMethod(ItcHolderDescriptor itcHolder, Type sourceType, MethodInfo method)
		{
			var actionInterceptors = itcHolder.Interceptors
									.Select(x => MapInterceptor(x, sourceType, method))
									.Where(x => x.Scores.Length > 0)
									.ToArray();

			actionInterceptors = actionInterceptors.Where(x => !FilteredByAttribute(itcHolder, sourceType, x)).ToArray();
			actionInterceptors = actionInterceptors.Where(x => !FilteredByMethod(x)).ToArray();
			var totalScoreHolder = new Dictionary<MethodsMapping, int>();
			var scoreBase = 0;
			foreach (var itc in actionInterceptors)
			{
				var max = itc.Scores.Max();
				if (scoreBase < max)
					scoreBase = max;
			}
			foreach (var itc in actionInterceptors)
			{
				int totalScore = 0;
				for (int i = 0; i < itc.Scores.Length; i++)
				{
					if (itc.Scores[i] == 0)
					{
						totalScore = 0;
						break;
					}
					totalScore += itc.Scores[i] * (int)Math.Pow(scoreBase, i);
				}
				totalScoreHolder.Add(itc, totalScore);
			}
			return totalScoreHolder.Where(x => x.Value > 0).OrderByDescending(x => x.Value).FirstOrDefault().Key;
		}
		private static MethodsMapping MapInterceptor(ItcMethodDefinition interceptorMethod, Type sourceType, MethodInfo method)
		{
			MethodsMapping methodMapping = new MethodsMapping()
			{
				InterceptorMethod = interceptorMethod,
				TargetMethod = method
			};
			ParameterMapping[] parametersMapping = null;

			var scores = new List<int>();
			// get return type scope
			scores.Add(GetAssignableLevel(interceptorMethod.ReturnType, method.ReturnType, AssignableLevelCalculationMode.OverallInterception));
			// get parameters score
			scores.Add(CalculateScoreForParameters(interceptorMethod.Parameters, method, out parametersMapping));
			// get target score
			scores.Add(CalculateScoreForTarget(interceptorMethod.TargetType, sourceType, method.DeclaringType));

			methodMapping.ParametersMapping = parametersMapping.ToArray();
			methodMapping.Scores = scores.ToArray();
			return methodMapping;
		}
		private static int GetAssignableLevel(Type expectedType, Type targetType
			, AssignableLevelCalculationMode calculationMode, Type interfaceToBeImplemented = null)
		{
			expectedType = expectedType.IsByRef
									? expectedType.GetElementType()
									: expectedType;

			targetType = targetType.IsByRef
									? targetType.GetElementType()
									: targetType;


			Type[] interfaces = null;
			Type itcType = null;

			if (!expectedType.IsGenericParameter)
			{
				if (expectedType.IsInterface)
				{
					itcType = typeof(object);
					interfaces = UnrollInterfaceHierarchy(expectedType);
				}
				else
				{
					itcType = expectedType;
					interfaces = expectedType.GetInterfaces();
				}
			}
			else
			{
				itcType = expectedType.BaseType != expectedType ? expectedType.BaseType : typeof(object);
				interfaces = expectedType.GetInterfaces().Union(itcType.GetInterfaces()).ToArray();
			}

			if (itcType == typeof(object) && targetType == typeof(object) && interfaces.Length == 0 && !expectedType.IsGenericParameter)
				return 2;
			if (itcType == typeof(object) && interfaces.Length == 0 && !expectedType.IsGenericParameter)
				return 1;
			if (itcType == typeof(object) && interfaces.Length == 0 && expectedType.IsGenericParameter)
				return 2;

			if (interfaceToBeImplemented != null)
			{
				bool interfaceMatchFound = false;
				foreach(var expectedInt in interfaces)
				{
					if (interfaceToBeImplemented.IsGenericType != expectedInt.IsGenericType)
						continue;
					if (!expectedInt.IsGenericType && interfaceToBeImplemented != expectedInt)
						continue;
					if (interfaceToBeImplemented.IsGenericType && interfaceToBeImplemented.GetGenericTypeDefinition() != expectedInt.GetGenericTypeDefinition())
						continue;

					interfaceMatchFound = true;
				}
				
				if (!interfaceMatchFound)
					return 0;
			}

			Type[] targetTypeInterfaces = !targetType.IsInterface ? targetType.GetInterfaces()
																 : UnrollInterfaceHierarchy(targetType);

			// expected type should be assignable from target type
			// and expected type should have the same generic definitions as base type
			if (itcType.IsGenericType 
				&& !itcType.GetGenericArguments().Any(x=>x.IsGenericParameter)
				&& !itcType.IsAssignableFrom(targetType))
				return 0;

			if (!itcType.IsGenericType 
				&& !itcType.IsAssignableFrom(targetType))
				return 0;

			if (itcType.IsGenericType &&
				ExtractGenericTypeForDefinition(targetType, itcType.GetGenericTypeDefinition()) == null)
				return 0;

			// expected type should implement all interfaces of target type
			var interfacesFound = 0;
			foreach(var actual in targetTypeInterfaces)
			{
				foreach (var expectedInt in interfaces)
				{
					if (actual.IsGenericType != expectedInt.IsGenericType)
						continue;
					if (!actual.IsGenericType && actual != expectedInt)
						continue;
					if (actual.IsGenericType && actual.GetGenericTypeDefinition() != expectedInt.GetGenericTypeDefinition())
						continue;
					++interfacesFound;
				}
			}
			if (interfacesFound != interfaces.Length)
				return 0;
			//------------------
			var sourceTypeHierarchy = GetTypeHierarchy(targetType);
			var itcTypeHierarchy = GetTypeHierarchy(itcType);

			int currentScore = 0;
			if (calculationMode == AssignableLevelCalculationMode.PointedInterception)
				currentScore = 4 + (targetTypeInterfaces.Length - interfaces.Length) * 2 + (sourceTypeHierarchy.Length - itcTypeHierarchy.Length) * 2 + (!expectedType.IsGenericParameter ? 1 : 0);
			else if (calculationMode == AssignableLevelCalculationMode.OverallInterception)
				currentScore = 4 + (interfaces.Length + itcTypeHierarchy.Length) * 2 + (!expectedType.IsGenericParameter ? 1 : 0);
			else throw new NotImplementedException();

			return currentScore;
		}
		private static int CalculateScoreForTarget(Type interceptorTarget, Type sourceType, Type interfaceType)
		{
			return GetAssignableLevel(interceptorTarget, sourceType, AssignableLevelCalculationMode.PointedInterception, interfaceType);
		}
		private static int CalculateScoreForParameters(ItcParameterDefinition[] itcParams, MethodInfo src, out ParameterMapping[] parametersMapping)
		{
			List<ParameterMapping> parametersMappingList = new List<ParameterMapping>();
			Func<ItcParameterDefinition, int> paramIndex = (ItcParameterDefinition x) => Array.IndexOf<ItcParameterDefinition>(itcParams, x);
			int result = 0;

			var itcParametersCount = itcParams.Length;
			var srcParameters = src.GetParameters();

			if (srcParameters.Length > 0 && itcParametersCount == 0)
				result = 0;
			else if (srcParameters.Length == 0 && itcParams.Count(x => !x.IsBatch) == 0)
				result = MAX_PARAMETER_MARK;
			else
			{
				var currentItcParameter = itcParams.First();

				for (int i = 0; i < srcParameters.Length; i++)
				{
					//== obtain parameters
					ItcParameterDefinition nextNotButchParameter = null;
					ItcParameterDefinition[] batchParameters = null;
					if (currentItcParameter.IsBatch)
					{
						nextNotButchParameter = itcParams.Skip(paramIndex(currentItcParameter))
														 .FirstOrDefault(x => !x.IsBatch);

						batchParameters = itcParams.Skip(paramIndex(currentItcParameter))
													.Take((nextNotButchParameter != null ? paramIndex(nextNotButchParameter) : itcParametersCount) - paramIndex(currentItcParameter))
													.ToArray();
					}

					var results = new Dictionary<ItcParameterDefinition, int>();
					//== calculate assignable level
					if (!currentItcParameter.IsBatch)
					{
						if (currentItcParameter.ParameterInfo.ParameterType.IsByRef != srcParameters[i].ParameterType.IsByRef ||
							currentItcParameter.ParameterInfo.IsOut != srcParameters[i].IsOut ||
							currentItcParameter.ParameterInfo.IsIn != srcParameters[i].IsIn)
							results.Add(currentItcParameter, 0);
						else
							results.Add(currentItcParameter, GetAssignableLevel(currentItcParameter.ParameterInfo.ParameterType, srcParameters[i].ParameterType, AssignableLevelCalculationMode.OverallInterception));
					}
					else
					{
						foreach (var batchParam in batchParameters)
						{
							results.Add(batchParam, GetAssignableLevel(batchParam.BatchType, srcParameters[i].ParameterType, AssignableLevelCalculationMode.OverallInterception));
						}
						if (nextNotButchParameter != null)
						{
							var temp = GetAssignableLevel(nextNotButchParameter.ParameterInfo.ParameterType, srcParameters[i].ParameterType, AssignableLevelCalculationMode.OverallInterception);
							if (temp != 0) ++temp;
							results.Add(nextNotButchParameter, temp);
						}
					}
					//== choose the most assignable parameter;
					var optimalParameter = results.First();
					foreach (var pair in results)
					{
						if (optimalParameter.Value < pair.Value)
							optimalParameter = pair;
					}
					result = optimalParameter.Value == 0 ? result = 0 : result += optimalParameter.Value;


					//////////////////////////
					int emptyBatchItcParametersCount = paramIndex(optimalParameter.Key) - paramIndex(currentItcParameter) - 1;
					emptyBatchItcParametersCount = emptyBatchItcParametersCount < 0 ? 0 : emptyBatchItcParametersCount;
					for (int j = paramIndex(currentItcParameter) + 1; j < (paramIndex(currentItcParameter) + 1) + emptyBatchItcParametersCount; j++)
					{
						ParameterMapping emptyMapping = new ParameterMapping();
						parametersMappingList.Add(emptyMapping);
						emptyMapping.ItcParameter = itcParams[j];
						emptyMapping.ItcParameterIndex = j;
					}
					ParameterMapping mapping = new ParameterMapping();
					parametersMappingList.Add(mapping);
					mapping.TargetParameter = srcParameters[i];
					mapping.TargetParameterIndex = i;
					mapping.ItcParameter = optimalParameter.Key;
					mapping.ItcParameterIndex = paramIndex(optimalParameter.Key);
					///////////////////////////////
					currentItcParameter = optimalParameter.Key.IsBatch ? optimalParameter.Key
																	  : itcParams.Skip(paramIndex(optimalParameter.Key) + 1)
																				  .FirstOrDefault();
                    // if this was the last itc parameter but we have more in source
                    if (currentItcParameter == null && (i + 1) < srcParameters.Length)
                        result = 0;

                    // if this parameter is the last in source and we have non batch parameters in itc
                    else if ((i + 1) == srcParameters.Length && currentItcParameter != null
                        && (!currentItcParameter.IsBatch
                        || itcParams.Skip(paramIndex(currentItcParameter)).Any(x => !x.IsBatch)))
                        result = 0;

					if (currentItcParameter == null)
						break;
					if (result == 0)
						break;
				}
				if (parametersMappingList.Count(x => !x.ItcParameter.IsBatch && CalculateCapacity(parametersMappingList, x) == 0) > 0)
					result = 0;
			}
			foreach (var itcParam in itcParams)
			{
				if (parametersMappingList.Where(x => x.ItcParameter == itcParam).Count() == 0)
				{
					ParameterMapping emptyMapping = new ParameterMapping();
					parametersMappingList.Add(emptyMapping);
					emptyMapping.ItcParameter = itcParam;
					emptyMapping.ItcParameterIndex = paramIndex(itcParam);
				}
			}
			parametersMapping = parametersMappingList.ToArray();
			return result;
		}
		private static Type[] GetTypeHierarchy(Type type)
		{
			List<Type> result = new List<Type>();
			if (!type.IsInterface)
			{
				result.Add(type);
				var current = type;
				while (current != typeof(object))
				{
					current = current.BaseType;
					result.Add(current);
				}
			}
			return result.ToArray();
		}
		#region filter by method
		private static bool FilteredByMethod(MethodsMapping methodsMapping)
		{
			if (methodsMapping.InterceptorMethod.Method.GetCustomAttributes(typeof(FilterByMethodAttribute), true).Length == 0)
				return false;
			var filterByMethod = (FilterByMethodAttribute)methodsMapping.InterceptorMethod.Method.GetCustomAttributes(typeof(FilterByMethodAttribute), true)[0];
			var regExp = String.IsNullOrEmpty(filterByMethod.RegExpPatternForMethodName)
							? methodsMapping.InterceptorMethod.Method.Name
							: filterByMethod.RegExpPatternForMethodName;
			return !Regex.Match(methodsMapping.TargetMethod.Name, regExp).Value.Equals(methodsMapping.TargetMethod.Name);
		}
		#endregion
		#region filter by attribute
		private static object[] GetAllMethodAttributes(MethodInfo sourceMethod, MethodInfo interfaceMethod)
		{
			var sourceMethodAttributes = sourceMethod.GetCustomAttributes(true).ToList();
			var sourceMethodAttrTypes = sourceMethodAttributes.Select(x => x.GetType());
			var interfaceMethodAttributes = interfaceMethod.GetCustomAttributes(true).ToList();

			foreach (var interfaceAttr in interfaceMethodAttributes)
			{
				if (!sourceMethodAttrTypes.Contains(interfaceAttr.GetType()))
					sourceMethodAttributes.Add(interfaceAttr);
			}
			return sourceMethodAttributes.ToArray();
		}
		private static bool FilteredByAttribute(ItcHolderDescriptor itcHolderDescriptor, Type sourceType, MethodsMapping methodsMapping)
		{
			if (methodsMapping.InterceptorMethod.Method.GetCustomAttributes(typeof(FilterByAttributeAttribute), true).Length == 0)
				return false;

			var filterByAttribute = (FilterByAttributeAttribute)methodsMapping.InterceptorMethod.Method.GetCustomAttributes(typeof(FilterByAttributeAttribute), true)[0];

			var interfaceMapping = sourceType.GetInterfaceMap(methodsMapping.TargetMethod.DeclaringType);

			MethodInfo sourceMethod = interfaceMapping.TargetMethods[Array.IndexOf(interfaceMapping.InterfaceMethods, methodsMapping.TargetMethod)];

			var sourceMethodAttributes = GetAllMethodAttributes(sourceMethod, methodsMapping.TargetMethod);

			switch (filterByAttribute.Type)
			{
				case FilterByAttributeAttribute.FilteringType.UseTypeArray:
					var attrTypes = sourceMethodAttributes.Select(x => x.GetType());
					foreach (var attrType in filterByAttribute.Attributes)
					{
						if (!attrTypes.Contains(attrType))
							return true;
					}
					break;
				case FilterByAttributeAttribute.FilteringType.UseMethodBasedFilter:

					string[] filterNames = filterByAttribute.FiltersNames.Length == 0
											? new[] { methodsMapping.TargetMethod.Name }
											: filterByAttribute.FiltersNames;

					var attrFilters = itcHolderDescriptor.AttributeFilters.Where(x => filterNames.Contains(x.FilterMethod.Name)).ToArray();
					foreach (var filter in attrFilters)
					{
						var attributes = sourceMethodAttributes.Where(x => x.GetType().Equals(filter.AttributeType)).ToArray();
						if (filter.IsArray)
						{
							var array = Array.CreateInstance(filter.AttributeType, attributes.Length);
							for (int i = 0; i < attributes.Length; i++)
							{
								array.SetValue(attributes[i], i);
							}
							if (!(bool)filter.FilterMethod.Invoke(null, new object[] { array }))
								return true;
						}
						else
						{
							if (!(bool)filter.FilterMethod.Invoke(null, new[] { attributes.Length>0
																					? attributes[0]
																					: null}))
								return true;
						}
					}

					break;
			}
			return false;
		}
		#endregion
		protected static Type DefyneGenericDelegateMethod(Type delegateType, Type actual, MethodInfo method)
		{
			var delMethod = delegateType.GetMethod("Invoke");
			var parameters = delMethod.GetParameters();
			var srcParameters = method.GetParameters();
			var genericDelParams = new Dictionary<Type, Type>();
			foreach (var arg in delegateType.GetGenericArguments())
			{
				genericDelParams.Add(arg, arg);
			}
			// result is T
			if (delMethod.ReturnType.IsGenericParameter)
			{
				genericDelParams[delMethod.ReturnType] = method.ReturnType;
			}
			// parameter is T
			for (int i = 0; i < parameters.Length; i++)
			{
				if (parameters[i].ParameterType.IsGenericParameter)
				{
					genericDelParams[parameters[i].ParameterType] = srcParameters[i].ParameterType;
				}
			}

	return !delegateType.IsGenericType 
				? delegateType
				: delegateType
					.GetGenericTypeDefinition()
					.MakeGenericType(genericDelParams.Select(x => x.Value != typeof(void) 
																	? x.Value 
																	: typeof(VoidResult))
					.ToArray());
		}

		private static Type ExtractGenericTypeForDefinition(Type actualType, Type genericDefinition)
		{
			var actual = actualType;
			do
			{
				if (actual.IsGenericType && actual.GetGenericTypeDefinition() == genericDefinition)
					return actual;
				actual = actual.BaseType;
			} while (actual != typeof(object));

			foreach (var actualInterface in actualType.GetInterfaces())
			{
				if (actualInterface.IsGenericType && actualInterface.GetGenericTypeDefinition() == genericDefinition)
					return actualInterface;
			}
			return null;
		}
		protected static MethodInfo DefyneGenericItcMethod(Type expected, MethodInfo itcMethod, Type actual, MethodInfo method)
		{
			var parameters = itcMethod.GetParameters();
			var srcParameters = method.GetParameters();
			var genericItcParams = new Dictionary<Type, Type>();
			foreach (var arg in itcMethod.GetGenericArguments())
			{
				genericItcParams.Add(arg, arg);
			}
			// result is T
			if (itcMethod.ReturnType.IsGenericParameter)
			{
				genericItcParams[itcMethod.ReturnType] = method.ReturnType;
			}

			// target is T
			if (parameters[TARGET_PARAM_INDEX].ParameterType.IsGenericParameter)
			{
				genericItcParams[parameters[TARGET_PARAM_INDEX].ParameterType] = actual;
			}
			// target is SomeClass<T>

			var expectedGenericArgs = expected.GetGenericArguments();

			if (expected.IsGenericType &&
				expectedGenericArgs.Any(x => x.IsGenericParameter))
			{
				Type actualGenericDefinition = ExtractGenericTypeForDefinition(actual, expected.GetGenericTypeDefinition());
				var actualGenericArgs = actualGenericDefinition.GetGenericArguments();
				for (int i = 0; i < expectedGenericArgs.Length; i++)
				{
					if (expectedGenericArgs[i].IsGenericParameter)
						genericItcParams[expectedGenericArgs[i]] = actualGenericArgs[i];
				}
			}
			// parameter is T
			var gp = parameters.Skip(CONTEXT_INVOKER_PARAM_INDEX + 1).ToArray();
			for (int i = 0; i < gp.Length; i++)
			{
				if (gp[i].ParameterType.IsGenericParameter)
				{
					genericItcParams[gp[i].ParameterType] = srcParameters[i].ParameterType;
				}
			}

			return itcMethod.MakeGenericMethod(genericItcParams.Values.Select(x => x != typeof(void) ? x : typeof(VoidResult)).ToArray());
		}
		protected static void PutObjectArrayParametersArrayOntoStack(ILGenerator methIL, MethodInfo method, int fromParameter, Type batchType, int capacity)
		{
			var parameters = method.GetParameters();
			methIL.EmitLdc_I4(capacity);
			methIL.Emit(OpCodes.Newarr, batchType);

			for (int i = fromParameter; i < (fromParameter + capacity); i++)
			{
				bool isByRef = parameters[i].ParameterType.IsByRef;
				var parameterType = isByRef
										? parameters[i].ParameterType.GetElementType()
										: parameters[i].ParameterType;
				methIL.Emit(OpCodes.Dup);
				methIL.EmitLdc_I4(i - fromParameter);
				methIL.EmitLdarg((short)(i + 1));

				if (isByRef)
					methIL.EmitLdInd(parameterType);

				if (parameterType.IsValueType &&
						!batchType.IsValueType)
					methIL.Emit(OpCodes.Box, parameterType);
				if (batchType.IsValueType)
					methIL.Emit(OpCodes.Stelem, batchType);
				else
					methIL.Emit(OpCodes.Stelem_Ref);
			}
		}
		protected static Type[] UnrollInterfacesHierarchy(Type[] interfaces)
		{
			List<Type> result = new List<Type>();
			foreach (var i in interfaces)
			{
				result.AddRange(UnrollInterfaceHierarchy(i));
			}
			return result.ToArray();
		}

		protected static Type[] UnrollInterfaceHierarchy(Type i)
		{
			List<Type> result = new List<Type>();
			result.Add(i);
			result.AddRange(i.GetInterfaces());
			return result.ToArray();
		}

		protected static int CalculateCapacity(IEnumerable<ParameterMapping> parameterMapping, ParameterMapping map)
		{
			return parameterMapping.Where(pMap => pMap.ItcParameter == map.ItcParameter && pMap.IsLinkedItcParameter)
								   .Count();
		}
	}
}
